簡單介紹useReducer。
當 state 較為複雜時可以透過 useReducer 來把更新狀態的邏輯一個個拆開,方便管理。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
reducer
: reducer 是一個 function,這個 function 可以接收到兩個東西一個是當下最新的 state 跟透過 dispatch
function 傳進去的參數。initialArg
: 初始資料,如果沒有 init
這個參數的話會被直接放到 state。init?
: 這是一個 function 可以接到 initialArg
如果初始資料需要經過計算或是處理的話就會使用到這個 function,initialArg
會被放到 init
function 裡,經過處理後才會變成 state。
state
: 當前最新的 state,跟 useState
的回傳值相同。dispatch
: 用來啟用 reducer
來修改 state 並觸發 re-render。通常 dispatch 會接收一個物件,通常這個物件會有一個 type 屬性,用來定義這一次的 dispatch 觸發什麼事件。
useReducer hook 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡,如果有需要可以建立一個新的子元件,放在子元件裡。
嚴格模式開啟時,在開發模式下,react 會在畫面第一次 render 時下快速的進行 mount -> unmount -> mount 的動作確保沒有多餘的 side effect 而發生錯誤,這個動作不應該對發生任何預期外的錯誤。
簡單的看過 useReducer
裡面的每一個參數跟 return 的 value 之後就來看一下範例吧。
來簡單的做一個計數器。
// reducer 有哪一些 action
const enum ActionType {
INCREASE = "INCREASE",
DECREASE = "DECREASE",
}
// dispatch 接收哪些參數
type CountAction = {
type: ActionType;
payload?: number;
};
// state
type CountObj = {
value: number;
};
function countReducer(state: CountObj, action: CountAction): CountObj {
switch (action.type) {
case ActionType.INCREASE: {
return { value: state.value + 1 };
}
case ActionType.DECREASE: {
return { value: state.value - 1 };
}
default:
return state;
}
}
這邊我定義了一個 countReducer
,上面有提到 reducer 會接收兩個參數 state
是當前最新的 value,action
則是透過 dispatch 傳進來的參數,這個參數在慣例上會有 type 屬性,用來判斷要做什麼動作。
為了方便閱讀慣例上也會使用 switch 判斷式來對 type 做判斷,像上面這樣,每一個 action 應該回傳一個新的物件來改變 state,跟 useState
一樣 react 會使用 Object.is()
來做比較,所以不要使用 mutable 的方式修改 state,這樣不會觸發 re-render。
完整的 code 如下。
import { useReducer } from "react";
// reducer 有哪一些 action
const enum ActionType {
INCREASE = "INCREASE",
DECREASE = "DECREASE",
}
// dispatch 接收哪些參數
type CountAction = {
type: ActionType;
payload?: number;
};
// state
type CountObj = {
value: number;
};
const initNumber: CountObj = {
value: 0,
};
// reducer
function countReducer(state: CountObj, action: CountAction): CountObj {
switch (action.type) {
case ActionType.INCREASE: {
return { value: state.value + 1 };
}
case ActionType.DECREASE: {
return { value: state.value - 1 };
}
default:
return state;
}
}
function App() {
const [count, dispatch] = useReducer(countReducer, initNumber);
function handleIncrease() {
dispatch({ type: ActionType.INCREASE });
}
function handleDecrease() {
dispatch({ type: ActionType.DECREASE });
}
return (
<div>
<h1>Count:{count.value}</h1>
<button onClick={handleIncrease}>increase</button>
<button onClick={handleDecrease}>decrease</button>
</div>
);
}
或許會覺得這樣的功能用 useState 來實現可能還比較快,比較容易閱讀,但是如果有一個比較複雜的狀態出現時就不同了。
假設我開了一間咖啡廳,營業中會有下面這些情形。
會有下面這些行為。
const enum ActionType {
SELL_COFFEE = "SELL_COFFEE", // 賣咖啡
SELL_COFFEE_BY_NUM = "SELL_COFFEE_BY_NUM", // 賣 N 杯咖啡
MAKE_COFFEE = "MAKE_COFFEE", // 煮咖啡
MAKE_COFFEE_BY_NUM = "MAKE_COFFEE_BY_NUM", // 煮 N 杯咖啡
REPLENISHMENT = "REPLENISHMENT", // 補豆子
}
reducer 這裡包含了上面所有的邏輯。
const initialState = {
coffeeBeans: 10, // 咖啡豆 10 包
coffee: 3, // 咖啡 3 杯
revenue: 1000, // 營業資金 1000 元
};
const reducer = (state: State, action: ReducerAction) => {
switch (action.type) {
case ActionType.SELL_COFFEE: // 賣咖啡
return {
...state,
coffee: state.coffee - 1, // 賣掉 1 杯咖啡
revenue: state.revenue + 80, // 1 杯 80 元
};
case ActionType.SELL_COFFEE_BY_NUM: // 賣 N 杯咖啡
return {
...state,
coffee: state.coffee - (action.num || 0), // 減掉賣出數量
revenue: state.revenue + (action.num || 0) * 80, // 賣出數量 * 80 元
};
case ActionType.MAKE_COFFEE: // 煮咖啡
return {
...state,
coffeeBeans: state.coffeeBeans - 2, // 消耗 2 包豆子
coffee: state.coffee + 1, // 增加 1 杯咖啡
};
case ActionType.MAKE_COFFEE_BY_NUM: // 煮 N 杯咖啡
return {
...state,
coffeeBeans: state.coffeeBeans - 2 * (action.num || 0), // 消耗 N * num 包豆子
coffee: state.coffee + 1 * (action.num || 0), // 增加 N 杯咖啡
};
case ActionType.REPLENISHMENT: // 補充咖啡豆
return {
...state,
coffeeBeans: state.coffeeBeans + 10, // 進貨 10 包咖啡豆
revenue: state.revenue - 200, // 花費 200 元
};
default:
return state;
}
};
而元件本身長這樣。
function App() {
const [number, setNumber] = useState<number>(1);
const [state, dispatch] = useReducer(reducer, initialState);
function handleCoffeeNumber({ target }: ChangeEvent<HTMLInputElement>) {
setNumber(Number(target.value));
}
function handleSellCoffee() {
// 當賣咖啡的時候已經沒有咖啡了的話,就先煮 5 杯咖啡再賣。
if (state.coffee === 0) {
dispatch({ type: ActionType.MAKE_COFFEE_BY_NUM, num: 5 });
}
dispatch({ type: ActionType.SELL_COFFEE });
}
function handleSellCoffeeByNum() {
dispatch({ type: ActionType.SELL_COFFEE_BY_NUM, num: number });
}
function handleMakeCoffee() {
dispatch({ type: ActionType.MAKE_COFFEE });
}
function handleReplenishment() {
dispatch({ type: ActionType.REPLENISHMENT });
}
return (
<div>
<h1>useReducer</h1>
<div>
<p>咖啡豆: {state.coffeeBeans} 包</p>
<p>咖啡: {state.coffee} 杯</p>
<p>營業額: {state.revenue} 元</p>
<label>
預定數量:
<input value={number} onChange={handleCoffeeNumber} />
</label>
</div>
<div>
<button onClick={handleSellCoffee}>賣咖啡</button>
<button onClick={handleSellCoffeeByNum}>賣 N 杯</button>
<button onClick={handleMakeCoffee}>煮咖啡</button>
<button onClick={handleReplenishment}>補咖啡豆</button>
</div>
</div>
);
}
如果要把上面 reducer 的內容全部放到元件裡面的話,元件裡面會變的非常冗長,不好閱讀跟維護。
useReducer 讓我們可以把更新狀態的邏輯跟 function handle 拆開,透過 dispatch 的方式來觸發狀態的更新,讓狀態的維護跟修改可以更清楚。
useReducer - react document
下一篇會簡單介紹 useLayoutEffect。
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium